# 交叉验证
"""
交叉验证（cross-validation）是一种评估泛化性能的统计学方法，它比单次划分训练集和测试集的方法更加稳定、全面。在交叉验证中，数据被多次划分，并且需要训练多个模型。
最常用的交叉验证是 k 折交叉验证（k-fold cross-validation），其中 k 是由用户指定的数字，
通常取 5 或 10。在执行 5 折交叉验证时，首先将数据划分为（大致）相等的 5 部分，每一
部分叫作折（fold）。接下来训练一系列模型。使用第 1 折作为测试集、其他折（2~5）作
为训练集来训练第一个模型。利用 2~5 折中的数据来构建模型，然后在 1 折上评估精度。
之后构建另一个模型，这次使用 2 折作为测试集，1、3、4、5 折中的数据作为训练集。利
用 3、4、5 折作为测试集继续重复这一过程。对于将数据划分为训练集和测试集的这 5 次
划分，每一次都要计算精度。最后我们得到了 5 个精度值。

"""
from audioop import cross

import mglearn.plots
from matplotlib import pyplot as plt
from sklearn.datasets import load_iris, make_blobs
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, KFold, LeaveOneOut, ShuffleSplit, GroupKFold

# 整个过程如图

mglearn.plots.plot_cross_validation()
# plt.show()

# scikit-learn中的交叉验证
'''
scikit-learn 是利用 model_selection 模块中的 cross_val_score 函数来实现交叉验证的。
cross_val_score 函数的参数是我们想要评估的模型、训练数据与真实标签。我们在 iris
数据集上对 LogisticRegression 进行评估
'''

iris = load_iris()
logreg = LogisticRegression()
scores = cross_val_score(logreg, iris.data, iris.target)
print("Cross-validation scores: {}".format(scores))
# Cross-validation scores: [0.96666667 1.         0.93333333 0.96666667 1.        ]

'''
默认情况下，cross_val_score 执行 3 折交叉验证，返回 3 个精度值。可以通过修改 cv 参
数来改变折数：
'''
scores = cross_val_score(logreg, iris.data, iris.target, cv=5)
print("Cross-validation scores: {}".format(scores))

'''
总结交叉验证精度的一种常用方法是计算平均值：
'''

print("Average cross-validation score: {:.2f}".format(scores.mean()))

'''
我们可以从交叉验证平均值中得出结论，我们预计模型的平均精度约为 96%。观察 5 折交
叉验证得到的所有 5 个精度值，我们还可以发现，折与折之间的精度有较大的变化，范围
为从 100% 精度到 90% 精度。这可能意味着模型强烈依赖于将某个折用于训练，但也可能
只是因为数据集的数据量太小。
'''

# 交叉验证的优点
'''
交叉验证相比单次划分训练集和测试集有以下优点：
避免随机划分的偏差：单次随机划分可能导致测试集精度不切实际地高或低，而交叉验证让每个样本在测试集中出现一次，模型需对所有样本泛化好，才能使交叉验证得分高。
提供模型对训练集选择的敏感性信息：多次划分可观察模型在不同数据划分下的表现范围，如iris数据集精度在90%到100%之间，反映模型在最坏和最好情况下的可能表现。
更高效地使用数据：相比单次划分，交叉验证在每次迭代中可使用更多数据拟合模型，如5折交叉验证用80%数据，10折用90%，通常能得到更精确的模型。
其主要缺点是增加了计算成本，需训练k个模型，速度比单次划分慢约k倍

'''

'''
交叉验证不是用来构建新模型的，它不返回模型。调用cross_val_score时，虽然会构建多个模型，但它的目的是评估算法在特定数据集上的泛化性能。
'''

# 分层k折交叉验证和其他策略
'''
将数据集划分为 k 折时，从数据的前 k 分之一开始划分（正如上一节所述），这可能并不总是一个好主意。例如，我们来看一下 iris 数据集：
'''

iris = load_iris()
print("Iris labels:\n{}".format(iris.target))

'''
数据分为三部分，每部分含一个类别。3折交叉验证时，每折只含一个类别，导致训练集和测试集类别不一致，精度为0，不如iris数据集的精度

在分类问题中，scikit-learn采用分层k折交叉验证，确保每个折中各类别的比例与整个数据集一致，避免简单k折策略导致的类别不均衡问题。
'''

mglearn.plots.plot_stratified_cross_validation()
# plt.show()

'''
例如，若90%样本属类别A，10%属类别B，分层交叉验证可确保每折中样本比例相同。使用分层k折交叉验证评估分类器通常更可靠，
因为标准k折交叉验证可能导致某折仅含类别A样本，无法反映分类器整体性能。

对于回归问题，scikit-learn 默认采用标准 k 折交叉验证。虽然可以尝试让每个折代表回归目标的不同取值，但这并非常见做法，也可能让用户感到意外。
'''


# 1. 对交叉验证的更多控制
'''
在 scikit-learn 中，可以通过 cv 参数来控制 cross_val_score 使用的折数。
此外，还可以通过提供一个交叉验证分离器（cross-validation splitter）来更精细地控制数据划分过程。
对于大多数场景，回归问题的默认 k 折交叉验证和分类问题的分层 k 折交叉验证表现良好，但在某些情况下，可能需要使用不同的策略。
例如，如果想要在分类数据集上使用标准 k 折交叉验证来重现他人结果，可以按照以下步骤操作：
 从 model_selection 模块中导入 KFold 分离器类。
 使用所需的折数实例化 KFold 分离器。
'''

kfold = KFold(n_splits=5)

# 然后我们可以将 kfold 分离器对象作为 cv 参数传入 cross_val_score
print("Cross-validation scores:\n{}".format(
 cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
# Cross-validation scores:
# [1.         1.         0.86666667 0.93333333 0.83333333]

# 通过这种方法，我们可以验证，在 iris 数据集上使用 3 折交叉验证（不分层）确实是一个
# 非常糟糕的主意：

kfold = KFold(n_splits=3)
print("Cross-validation scores:\n{}".format(
 cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
# Cross-validation scores:
# [0. 0. 0.]

'''
在 iris 数据集中，每个折对应一个类别，导致模型无法学习。为了解决这个问题，可以将数据打乱，而不是使用分层。
这可以通过将 KFold 的 shuffle 参数设置为 True 来实现。同时，为了获得可重复的打乱结果，需要固定 random_state。
否则，每次运行 cross_val_score 时都会得到不同的结果，因为每次使用的划分不同。打乱数据可以得到更好的结果。
'''

kfold = KFold(n_splits=3, random_state=0,shuffle=True)
print("Cross-validation scores:\n{}".format(cross_val_score(logreg,iris.data,iris.target,cv=kfold)))
# Cross-validation scores:
# [0.98 0.96 0.96]

# 2. 留一法交叉验证
'''
留一法交叉验证（Leave-One-Out Cross-Validation, LOOCV）是一种特殊的交叉验证方法，可以看作是每折只包含单个样本的 k 折交叉验证。
在每次划分中，选择一个数据点作为测试集，其余数据作为训练集。这种方法在小型数据集上可以给出较好的估计结果，但对大型数据集来说计算成本较高，非常耗时。
'''

loo = LeaveOneOut()
scores = cross_val_score(logreg,iris.data,iris.target,cv=loo)
print("Number of cv iterations: ",len(scores))
print("Mean accuracy: {:.2f}".format(scores.mean()))
# Number of cv iterations:  150
# Mean accuracy: 0.97

# 3. 打乱划分交叉验证
'''
打乱划分交叉验证是一种灵活的交叉验证方法。每次划分时，随机选取train_size个点作为训练集和test_size个点作为测试集，这两个集合互不相交。
这个过程重复n_iter次。例如，对于一个包含10个点的数据集，进行4次迭代划分，每次训练集有5个点，测试集有2个点。
train_size和test_size可以是整数，表示集合的绝对大小，也可以是浮点数，表示占整个数据集的比例
'''

mglearn.plots.plot_shuffle_split()
# plt.show()

# 下面的代码将数据集划分为 50% 的训练集和 50% 的测试集，共运行 10 次迭代
shuffle_split = ShuffleSplit(test_size=.5,train_size=.5,n_splits=10)
scores = cross_val_score(logreg,iris.data,iris.target,cv=shuffle_split)
print("Cross-validation scores:\n{}".format(scores))
# Cross-validation scores:
# [0.94666667 0.94666667 0.90666667 0.96       0.97333333 0.96
#  0.97333333 0.93333333 0.96       0.98666667]

'''
打乱划分交叉验证（ShuffleSplit）具有以下特点：
 独立控制迭代次数：可以在指定训练集和测试集大小的同时，独立控制迭代次数（n_iter），这在某些情况下非常有用。
 部分数据使用：通过设置train_size与test_size之和不等于1，可以在每次迭代中仅使用部分数据，对大型数据集上的试验尤其有帮助。
 分层形式：ShuffleSplit还有一种分层的形式，名为StratifiedShuffleSplit，它可以在分类任务中提供更可靠的结果，确保每个折中各类别的比例与整个数据集一致。
'''

# 4. 分组交叉验证
'''
另一种非常常见的交叉验证适用于数据中的分组高度相关时。比如你想构建一个从人脸图
片中识别情感的系统，并且收集了 100 个人的照片的数据集，其中每个人都进行了多次拍
摄，分别展示了不同的情感。我们的目标是构建一个分类器，能够正确识别未包含在数据
集中的人的情感。你可以使用默认的分层交叉验证来度量分类器的性能。但是这样的话，
同一个人的照片可能会同时出现在训练集和测试集中。对于分类器而言，检测训练集中出
现过的人脸情感比全新的人脸要容易得多。因此，为了准确评估模型对新的人脸的泛化能
力，我们必须确保训练集和测试集中包含不同人的图像。

为了实现这一点，我们可以使用 GroupKFold，它以 groups 数组作为参数，可以用来说明照
片中对应的是哪个人。这里的 groups 数组表示数据中的分组，在创建训练集和测试集的时
候不应该将其分开，也不应该与类别标签弄混。

数据分组的这种例子常见于医疗应用，你可能拥有来自同一名病人的多个样本，但想要将
其泛化到新的病人。同样，在语音识别领域，你的数据集中可能包含同一名发言人的多条
记录，但你希望能够识别新的发言人的讲话。

下面这个示例用到了一个由 groups 数组指定分组的模拟数据集。这个数据集包含 12 个数
据点，且对于每个数据点，groups 指定了该点所属的分组（想想病人的例子）。一共分成 4
个组，前 3 个样本属于第一组，接下来的 4 个样本属于第二组，以此类推：
'''
X,y = make_blobs(n_samples=12,random_state=0)
groups = [0,0,0,1,1,1,1,2,2,3,3,3]
scores = cross_val_score(logreg,X,y,cv=GroupKFold(n_splits=3))
print("Cross-validation scores:\n{}".format(scores))

# 如你所见，对于每次划分，每个分组都是整体出现在训练集或测试集中：

mglearn.plots.plot_group_kfold()

'''
scikit-learn 中还有很多交叉验证的划分策略，适用于更多的使用场景 [ 你可以在 scikitlearn 的用户指南页面查看这些内容（http://scikit-learn.org/stable/modules/cross_validation.
html）]。但标准的 KFold、StratifiedKFold 和 GroupKFold 是目前最常用的几种。
'''